﻿namespace Blip.JSONParser
{
    using System.Collections.Generic;

    /// <summary>
    /// Parser formatu JavaScript Object Notation.
    /// </summary>
    /// <seealso cref="SyntaxError"/>
    public class JSON
    {
        /// <summary>
        /// Długość parsowanego ciągu.
        /// </summary>
        protected readonly int Length;

        /// <summary>
        /// Typ wyliczeniowy definiujący stałe symbole.
        /// </summary>
        protected enum TOKEN : byte { COMMA, OPEN_BRACKET, CLOSE_BRACKET, COLON, QUOTE, OPEN_ARRAY, CLOSE_ARRAY, DIGIT, BOOLEAN, NULL, VALUE, END };

        /// <summary>
        /// Parsowany tekst.
        /// </summary>
        protected string Text { get; private set; }

        /// <summary>
        /// Aktualnie odczytywany indeks.
        /// </summary>
        protected int Index { get; set; }

        /// <summary>
        /// Ostatnio dopasowany symbol.
        /// </summary>
        protected TOKEN Token { get; set; }

        /// <summary>
        /// Aktualnie odczytywany znak.
        /// </summary>
        protected char Current
        {
            get
            {
                if (Index >= Length)
                    return '\0';
                return Text[Index];
            }
        }

        /// <summary>
        /// Dopasowuje następny stały symbol.
        /// </summary>
        /// <returns>Dopasowany symbol</returns>
        /// <seealso cref="TOKEN"/>
        protected TOKEN NextToken()
        {
            SkipWhiteSpaces();

            switch (Current)
            {
                case '\0': Token = TOKEN.END; break;
                case '{': Token = TOKEN.OPEN_BRACKET; break;
                case '}': Token = TOKEN.CLOSE_BRACKET; break;
                case '[': Token = TOKEN.OPEN_ARRAY; break;
                case ']': Token = TOKEN.CLOSE_ARRAY; break;
                case ':': Token = TOKEN.COLON; break;
                case '"':
                case '\'': Token = TOKEN.QUOTE; break;
                case ',': Token = TOKEN.COMMA; break;
                case '.':
                case '+':
                case '-': Token = TOKEN.DIGIT; break;
                default:

                    if (char.IsDigit(Current))
                        Token = TOKEN.DIGIT;

                    else if (MatchString("true") || MatchString("false"))
                        Token = TOKEN.BOOLEAN;

                    else if (MatchString("null"))
                        Token = TOKEN.NULL;

                    else
                        Token = TOKEN.VALUE;

                    break;
            }

            return Token;
        }

        /// <summary>
        /// Porównuje odczytany token z wartością oczekiwaną.
        /// </summary>
        /// <param name="token">Odczytany symbol</param>
        /// <exception cref="SyntaxError">Sygnalizuje błąd składniowy</exception>
        protected void Match(TOKEN token)
        {
            if (token != Token)
                throw new SyntaxError("Unexpected token");

            ++Index;
            NextToken();
        }

        /// <summary>
        /// Opuszcza białe znaki.
        /// </summary>
        protected void SkipWhiteSpaces()
        {
            while (Index < Length && char.IsWhiteSpace(Current))
                ++Index;
        }

        /// <summary>
        /// Dopasowuje wzorzec.
        /// </summary>
        /// <param name="pattern">Wzorzec do dopasowania</param>
        /// <returns>Prawda, gdy kolejno odczytane znaki pasują do wzorca</returns>
        protected bool MatchString(string pattern)
        {
            if (Index + pattern.Length < Length && Text.Substring(Index, pattern.Length).ToLower() == pattern)
                return true;
            return false;
        }

        /// <summary>
        /// Parser wyrażeń numerycznych.
        /// </summary>
        /// <returns>Wartość wyrażenia</returns>
        protected virtual double ParseExpression()
        {
            int start = Index;

            char[] tab = { '+', '-', '*', '/', '.', '(', ')' };
            while (char.IsDigit(Current) || char.IsWhiteSpace(Current) || System.Array.Exists(tab, x => x == Current))
                ++Index;

            string expr = Text.Substring(start, Index - start).Trim();

            double value = EvalExpression(expr);

            NextToken();
            return value;
        }

        /// <summary>
        /// Oblicza wartość wyrażenia numerycznego.
        /// </summary>
        /// <param name="expr">Wyrażenie zapisane w postaci łańcucha.</param>
        /// <returns>Wartość wyrażenia</returns>
        protected double EvalExpression(string expr)
        {
            return double.Parse(expr.Replace('.', ','));
        }

        /// <summary>
        /// Parsuje wyrażenie logiczne.
        /// </summary>
        /// <returns>Wartość logiczna odpowiadająca odczytanemu łańcuchowi</returns>
        protected virtual bool ParseBoolean()
        {
            if (MatchString("true"))
            {
                Index += 4;
                NextToken();
                return true;
            }
            else
            {
                Index += 5;
                NextToken();
                return false;
            }
        }

        /// <summary>
        /// Parsuje wartość NULL.
        /// </summary>
        /// <returns>Wartość null</returns>
        protected virtual object ParseNull()
        {
            Index += 4;
            NextToken();
            return null;
        }
        
        /// <summary>
        /// Parsuje łańcuch znakowy pomiędzy znakami "".
        /// </summary>
        /// <returns>Odczytany łańcuch znakowy</returns>
        protected virtual string ParseString()
        {
            Match(TOKEN.QUOTE);

            char quoteMark;
            do
            {
                --Index;
                quoteMark = Current;
            } while (char.IsWhiteSpace(quoteMark));
            ++Index;

            short slashes = 0;
            int start = Index;
            int chars = 0;

            while (Index < Length)
            {
                ++chars;
                if (Current == '\\')
                {
                    ++slashes;
                }
                else if (Current == quoteMark && slashes % 2 == 0)
                {
                    --Index;
                    break;
                }
                else
                {
                    slashes = 0;
                }
                ++Index;
            }

            Index++;
            NextToken();

            Match(TOKEN.QUOTE);

            return Converter.Convert(Text.Substring(start, chars - 1));
        }

        /// <summary>
        /// Parsuje obiekt do postaci JSON.
        /// </summary>
        /// <returns>Słownik zawieranych obiektów</returns>
        protected virtual IDictionary<string, dynamic> ParseObject()
        {
            var json = new Dictionary<string, dynamic>();

            Match(TOKEN.OPEN_BRACKET);

            if (Token != TOKEN.CLOSE_BRACKET)
            {
                do
                {
                    string key = ParseString();

                    Match(TOKEN.COLON);

                    json[key] = GetValue();

                    if (Token == TOKEN.COMMA)
                        Match(TOKEN.COMMA);
                    else
                        break;

                } while (true);
            }

            Match(TOKEN.CLOSE_BRACKET);

            return json;
        }

        /// <summary>
        /// Parsuje tablicę obiektów.
        /// </summary>
        /// <returns>Tablica obiektów</returns>
        protected virtual dynamic[] ParseArray()
        {
            var json = new List<dynamic>();

            Match(TOKEN.OPEN_ARRAY);

            if (Token != TOKEN.CLOSE_ARRAY)
            {
                do
                {
                    json.Add(GetValue());

                    if (Token == TOKEN.COMMA)
                        Match(TOKEN.COMMA);
                    else
                        break;

                } while (true);
            }

            Match(TOKEN.CLOSE_ARRAY);

            dynamic[] tab = new dynamic[json.Count];
            json.CopyTo(tab, 0);

            return tab;
        }

        /// <summary>
        /// Odczytuje wartość dla danego klucza.
        /// </summary>
        /// <returns>Obiekt JSON</returns>
        protected virtual dynamic GetValue()
        {
            switch (Token)
            {
                case TOKEN.OPEN_BRACKET:
                    return ParseObject();

                case TOKEN.OPEN_ARRAY:
                    return ParseArray();

                case TOKEN.DIGIT:
                    return ParseExpression();

                case TOKEN.QUOTE:
                    return ParseString();

                case TOKEN.BOOLEAN:
                    return ParseBoolean();

                case TOKEN.NULL:
                    return ParseNull();

                default:
                    throw new SyntaxError("No matching symbol found");
            }
        }

        /// <summary>
        /// Statyczna metoda, dostarczona dla wygody użytkowania.
        /// </summary>
        /// <param name="text">Ciąg JSON</param>
        /// <returns>Obiekt JSON</returns>
        /// <exception cref="SyntaxError">Sygnalizuje błąd składniowy</exception>
        /// <example>
        /// <code>
        /// string s = "{'a':1,'b':[1, 2, 3],'c':true}";
        /// dynamic json = JSON.Parse(s);
        /// </code>
        /// </example>
        public static dynamic Parse(string text)
        {
            return new JSON(text).Parse();
        }

        /// <summary>
        /// Metoda parsująca podany jako argument konstruktora ciąg znaków.
        /// </summary>
        /// <returns>Obiekt JSON</returns>
        /// <exception cref="SyntaxError">Sygnalizuje błąd składniowy</exception>
        /// <example>
        /// <code>
        /// string s = "{'a':1,'b':[1, 2, 3],'c':true}";
        /// JSONParser parser = new JSONParser(s);
        /// dynamic json = parser.Parse();
        /// </code>
        /// </example>
        public dynamic Parse()
        {
            NextToken();
            return GetValue();
        }

        /// <summary>
        /// Konstruktor inicjalizujący obiekt parsera.
        /// </summary>
        /// <param name="text">Ciąg JSON</param>
        /// <example>
        /// <code>
        /// string s = "{'a':1,'b':[1, 2, 3],'c':true}";
        /// JSONParser parser = new JSONParser(s);
        /// </code>
        /// </example>
        public JSON(string text)
        {
            Text = text.Trim();
            Index = 0;
            Length = Text.Length;
        }
    }
}
